iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
自我挑戰組

30 天打造 Accessible UI Kit:從 shadcn/ui 到自己的 Design System系列 第 21 篇

# Day 21: 資料列表不只有「有資料」:Loading、Empty、Error

  • 分享至 

  • xImage
  •  

Day 20,我們第一次把前面完成的元件真正組成一個 Data List:

Data List
│
├── Search
├── Result Count
├── Table
│   └── Badge
└── Pagination

畫面開始很像真正的系統:

申請紀錄

搜尋申請紀錄
[ 搜尋姓名、申請項目或狀態............ ]

共 237 筆資料

┌──────────────────────────────────────────┐
│ 姓名    申請項目    狀態       更新時間 │
├──────────────────────────────────────────┤
│ 王小明  學分抵免    [已通過]   10/01    │
│ 陳小華  休學申請    [待審核]   10/01    │
│ 林小美  獎學金申請  [已退回]   09/30    │
└──────────────────────────────────────────┘

          <  1  [2]  3  4  …  10  >

但到目前為止,我們其實偷偷做了一個非常樂觀的假設:

資料一定會成功取得。

真實世界當然沒有這麼配合。

API 可能還在處理:

Loading...

可能成功取得資料,但:

0 records

也可能直接:

500 Internal Server Error

甚至還有一種情況:

原本有資料
↓
搜尋
↓
0 results

這些狀態看起來都像:

「Table 沒有資料。」

但對使用者來說,它們代表的是完全不同的事情。

所以今天要處理的是:

Data State。


今天要完成什麼?

今天不急著做新的大型 Component。

我們會沿用前面已經完成的:

Table
Alert
Empty State
Button

再加入:

Skeleton

最後讓 Data List 能處理:

Data List
│
├── Loading
├── Success
├── Empty
├── No Results
└── Error

並處理幾個 Accessibility 問題:

aria-busy
aria-live
role="status"
role="alert"
Loading announcement
Error feedback
Focus
Retry

1. 先把 Data State 分清楚

在開始寫 JSX 以前,我想先把狀態畫出來。

我們現在的 Data List 其實可能處於:

                 Data List
                     │
        ┌────────────┼────────────┐
        ↓            ↓            ↓
     Loading       Success       Error
                     │
              ┌──────┼──────┐
              ↓      ↓      ↓
             Data  Empty  No Results

乍看之下:

Empty
No Results

很像。

但 Day 20 已經碰到這個問題:

Empty

資料來源本身就是空的:

applications.length === 0

例如:

目前還沒有任何申請紀錄。

No Results

原本有資料,但搜尋後沒有符合項目:

applications.length > 0

但:

filteredApplications.length === 0

例如:

找不到符合「王小明」的資料。

兩者雖然都顯示:

0 筆

但使用者下一步完全不同。


2. 為什麼不能全部寫成「目前沒有資料」?

假設我搜尋:

王小明

結果系統顯示:

目前沒有資料。

我可能會理解成:

這個系統本來就沒有任何申請紀錄?

但真正的情況只是:

搜尋條件沒有符合的結果。

反過來,如果系統真的一筆資料都沒有,卻顯示:

找不到符合搜尋條件的資料。

也很奇怪。

所以:

Empty
≠
No Results
≠
Error
≠
Loading

Accessibility 不只是:

ARIA
Keyboard
Screen Reader

讓使用者知道:

現在到底發生什麼事情。

本身就是 Accessibility 的一部分。


3. 先加入 Skeleton

今天第一個實作是 Loading State。

可以使用 shadcn/ui:

npx shadcn@latest add skeleton

會產生:

src/components/ui/skeleton.tsx

通常很簡單:

import { cn } from "cn"

function Skeleton({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="skeleton"
      className={cn(
        "animate-pulse rounded-md bg-accent",
        className
      )}
      {...props}
    />
  )
}

export { Skeleton }

照我們 CUI 的 Component Contract,補上:

data-cui-slot="skeleton"

最後:

<div
  data-slot="skeleton"
  data-cui-slot="skeleton"
  className={cn(
    "animate-pulse rounded-md bg-accent",
    className
  )}
  {...props}
/>

這樣未來 Legacy 也可以:

<div data-cui-slot="skeleton"></div>

4. Skeleton 本身不是「Loading」

這裡有一個很重要的差別。

Skeleton:

██████████
██████
██████████████

只是:

Loading 的視覺呈現。

它本身並沒有告訴輔助科技:

資料載入中

如果只是:

<Skeleton />
<Skeleton />
<Skeleton />

Screen Reader 很可能什麼都不知道。

所以:

Skeleton
≠
Loading semantics

Skeleton 負責 Visual。

Loading State 還需要額外的 Semantic Feedback。


5. 使用 aria-busy

如果 Data List 正在更新,可以在整個資料區域加:

<section
  aria-labelledby="application-list-title"
  aria-busy={isLoading}
>

當:

isLoading === true

HTML 會變成:

<section aria-busy="true">

這代表:

這個區域目前正在更新。

完成後:

<section aria-busy="false">

這非常適合:

Data List
Search Results
Dashboard Widget
Async Form

這類會動態更新內容的區域。


6. aria-busy 不等於 Loading Message

不過:

aria-busy="true"

並不是畫面上的 Loading 文字。

所以我們還是可以提供:

<p className="sr-only">
  申請紀錄載入中
</p>

甚至搭配:

role="status"

例如:

<div role="status">
  <span className="sr-only">
    申請紀錄載入中
  </span>

  <Skeleton />
  <Skeleton />
  <Skeleton />
</div>

這樣:

Visual
→ Skeleton

Semantic
→ 申請紀錄載入中

兩邊各自負責不同事情。


7. role="status" 和 role="alert" 不一樣

前面做 Alert 時我們已經碰過這件事。

今天在 Async Data 裡會更明顯。

role="status"

適合:

資料載入完成
搜尋完成
儲存完成
共有 10 筆資料

這類:

有用,但通常不需要立刻打斷使用者。

概念上接近:

aria-live="polite"

role="alert"

適合:

載入失敗
表單送出失敗
發生重要錯誤

通常比較需要立即被注意。

所以:

Loading
→ status / aria-busy

Error
→ alert

但同樣不是死規則。

要看訊息的重要程度。


8. 建立 Loading Table

我們可以讓 Loading State 保留 Table 的大致形狀。

例如:

<Table>
  <TableCaption className="sr-only">
    申請紀錄載入中
  </TableCaption>

  <TableHeader>
    <TableRow>
      <TableHead scope="col">姓名</TableHead>
      <TableHead scope="col">申請項目</TableHead>
      <TableHead scope="col">狀態</TableHead>
      <TableHead scope="col">更新時間</TableHead>
    </TableRow>
  </TableHeader>

  <TableBody>
    {Array.from({ length: 3 }).map((_, index) => (
      <TableRow key={index}>
        <TableCell>
          <Skeleton className="h-4 w-20" />
        </TableCell>

        <TableCell>
          <Skeleton className="h-4 w-28" />
        </TableCell>

        <TableCell>
          <Skeleton className="h-5 w-16 rounded-full" />
        </TableCell>

        <TableCell>
          <Skeleton className="h-4 w-24" />
        </TableCell>
      </TableRow>
    ))}
  </TableBody>
</Table>

畫面:

姓名       申請項目       狀態       更新時間
──────────────────────────────────────────
██████     █████████      █████      ███████
██████     █████████      █████      ███████
██████     █████████      █████      ███████

這比整張畫面突然變成:

Loading...

更能保留 Layout。


9. Skeleton 要不要被 Screen Reader 讀到?

通常不用。

因為:

Skeleton 1
Skeleton 2
Skeleton 3

對使用者沒有資訊價值。

真正有價值的是:

申請紀錄載入中

因此 Skeleton 本身可以保持純視覺。

如果 Skeleton container 會產生不必要的 Accessibility Tree 內容,也可以在 Loading visual wrapper 使用:

aria-hidden="true"

例如:

<div aria-hidden="true">
  <Skeleton />
  <Skeleton />
  <Skeleton />
</div>

但不要把真正的 Loading Message 一起藏掉。


10. 模擬 Loading State

目前還沒有真的串 API。

所以今天可以先用:

const [isLoading, setIsLoading] = useState(false)

然後暫時做一個 Demo Button:

<Button
  variant="outline"
  onClick={() => {
    setIsLoading(true)

    window.setTimeout(() => {
      setIsLoading(false)
    }, 2000)
  }}
>
  模擬載入
</Button>

這不是正式 Data List API。

只是讓我們可以真的測試:

Data
↓
Loading
↓
Data

而不是只看 Static Screenshot。


11. Loading 時 Search 要不要消失?

不一定。

如果是:

第一次進頁面

整個 Data List 還沒有任何資料,Search 可能暫時 Disabled。

但如果是:

使用者輸入 Search
↓
重新 Fetch

通常 Search 不應該突然消失。

可能只是:

<Input
  disabled={isLoading}
/>

或甚至仍然允許輸入,再搭配 debounce / request cancellation。

這些屬於 Application Behavior。

CUI 不需要替所有產品決定。

今天先保持簡單:

Heading
Search
Loading Table

Layout 不要因 Loading 大幅跳動。


12. 接著處理 Empty State

假設 API 成功:

200 OK

但回來:

[]

這不是 Error。

它只是:

目前沒有資料。

這時我們就可以使用 Day 17 做過的 Empty State。

例如:

<EmptyState>
  <EmptyStateTitle>
    尚無申請紀錄
  </EmptyStateTitle>

  <EmptyStateDescription>
    目前沒有任何申請資料。
  </EmptyStateDescription>
</EmptyState>

如果這個系統允許新增:

<EmptyStateAction>
  <Button>
    新增申請
  </Button>
</EmptyStateAction>

畫面:

        尚無申請紀錄

   目前沒有任何申請資料。

          [ 新增申請 ]

13. Empty State 不需要 role="alert"

這個很重要。

沒有資料

通常不是錯誤。

所以不要:

<EmptyState role="alert">

Screen Reader 沒必要被:

「警告!你沒有資料!」

嚇一跳 😂

Empty State 本身正常存在於 Document Flow 就可以。

如果它是 Async Loading 完成後才出現,而且真的需要通知,也可以根據情境使用比較溫和的:

status

但通常不需要硬加 ARIA。


14. No Results 再複習一次

如果:

applications.length > 0

但:

filteredApplications.length === 0

代表:

No Results

這時內容應該是:

<EmptyState>
  <EmptyStateTitle>
    找不到符合條件的資料
  </EmptyStateTitle>

  <EmptyStateDescription>
    請嘗試其他搜尋關鍵字。
  </EmptyStateDescription>

  <EmptyStateAction>
    <Button
      variant="outline"
      onClick={() => setQuery("")}
    >
      清除搜尋
    </Button>
  </EmptyStateAction>
</EmptyState>

所以:

Empty
→ 新增資料

No Results
→ 修改 / 清除搜尋

同一個 UI Pattern,可以服務不同狀態。


15. Error State

接著來到第三種:

API failed

例如:

500
Timeout
Network Error
Permission Error

這時不能顯示:

目前沒有資料

因為這會誤導使用者。

系統根本不知道:

到底有沒有資料。

它只是:

沒有成功取得資料。

因此應該明確顯示:

載入失敗

16. 使用 Day 17 的 Alert

我們已經有:

<Alert>

所以 Error State 可以:

<Alert variant="destructive">
  <AlertTitle>
    無法載入申請紀錄
  </AlertTitle>

  <AlertDescription>
    取得資料時發生錯誤,請稍後再試。
  </AlertDescription>
</Alert>

如果你的 Alert 已經依照 Day 17 的設計,讓 role 由使用端決定,可以:

<Alert
  variant="destructive"
  role="alert"
>

這正好證明當初:

不把 role="alert" 寫死在 Alert Component

是有價值的。

因為:

Alert visual component
≠
所有情況都是 assertive alert

17. Error Message 要不要直接顯示技術錯誤?

假設 API 回:

ORA-00001: unique constraint ...

或者:

TypeError: Failed to fetch

通常不要直接丟給一般使用者。

UI 可以顯示:

無法載入申請紀錄

取得資料時發生錯誤,請稍後再試。

詳細技術資訊:

HTTP Status
Stack Trace
Database Error
Request ID

可以留給:

Logging
Monitoring
Developer Console
Error Tracking

使用者需要知道的是:

發生什麼事?
我現在可以做什麼?

18. Error State 最重要的是下一步

只顯示:

載入失敗

其實幫助有限。

如果這個錯誤可以重試,就提供:

<Button
  variant="outline"
  onClick={handleRetry}
>
  重新載入
</Button>

組合:

<Alert
  variant="destructive"
  role="alert"
>
  <AlertTitle>
    無法載入申請紀錄
  </AlertTitle>

  <AlertDescription>
    取得資料時發生錯誤,請稍後再試。
  </AlertDescription>

  <AlertAction>
    <Button
      variant="outline"
      size="sm"
      onClick={handleRetry}
    >
      重新載入
    </Button>
  </AlertAction>
</Alert>

這時 Error State 就不是:

❌ Error

而是:

發生問題
+
下一步

19. Retry 之後會發生什麼?

假設使用者按:

重新載入

State 可以:

Error
 ↓
Loading
 ↓
Success

所以:

function handleRetry() {
  setError(null)
  setIsLoading(true)

  // fetch again
}

如果今天只是 Demo:

function handleRetry() {
  setError(false)
  setIsLoading(true)

  window.setTimeout(() => {
    setIsLoading(false)
  }, 2000)
}

之後真的串 API,再換成實際 fetch。


20. Loading 完成後需要自動 Focus Table 嗎?

通常:

不要。

假設使用者按:

搜尋

然後:

Loading
↓
Table

如果我們突然:

tableRef.current?.focus()

使用者的 Focus 會被強行從 Search 拉走。

這通常反而很干擾。

比較好的策略:

Focus 保持原位
+
用 status / live region 告知結果更新

例如:

找到 12 筆資料

而不是:

搜尋
↓
Focus 瞬移到 Table

21. Retry 是例外嗎?

假設 Error State 裡:

[重新載入]

使用者按下後:

Error
↓
Loading
↓
Success

原本的 Retry Button 消失了。

這時 Focus 可能會因 DOM 消失而回到:

body

這就需要依實際產品流程判斷。

可能的策略包括:

Loading container
Result heading
Search field

但今天先不自動搬 Focus。

因為 Focus Management 要根據:

操作來源
DOM 變化
後續任務

決定。

不要看到 Dynamic UI 就:

useEffect(() => {
  ref.current?.focus()
}, [state])

這種 Accessibility 魔法很容易反而害人。


22. 建立明確的 Data State

如果目前只有:

const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(false)

Demo 階段還可以。

但狀態越來越多:

Loading
Error
Success
Empty

就可能出現:

isLoading = true
error = true

那現在到底是哪個 State?

所以之後可以考慮:

type DataState =
  | "loading"
  | "success"
  | "error"

例如:

const [dataState, setDataState] =
  useState<DataState>("success")

然後:

if (dataState === "loading") {
  ...
}

if (dataState === "error") {
  ...
}

Empty / No Results 則可以從資料本身推導。


23. 不要把所有 State 都存進 State

例如:

const [isEmpty, setIsEmpty] = useState(false)
const [hasNoResults, setHasNoResults] = useState(false)

其實不一定需要。

因為:

const isEmpty =
  dataState === "success" &&
  applications.length === 0

以及:

const hasNoResults =
  dataState === "success" &&
  applications.length > 0 &&
  filteredApplications.length === 0

這些都可以從既有資料推導。

也就是:

Source State
→ loading / success / error

Derived State
→ empty / no results

避免多份 State 彼此不同步。


24. 最後的 Render Logic

最後 Data List 大概可以整理成:

<section
  aria-labelledby="application-list-title"
  aria-busy={dataState === "loading"}
  className="space-y-4"
>
  <div>
    <h2
      id="application-list-title"
      className="text-xl font-semibold"
    >
      申請紀錄
    </h2>

    <p className="text-sm text-muted-foreground">
      查看目前的申請與審核狀態。
    </p>
  </div>

  <Field>
    <FieldLabel htmlFor="application-search">
      搜尋申請紀錄
    </FieldLabel>

    <Input
      id="application-search"
      type="search"
      value={query}
      onChange={(event) => setQuery(event.target.value)}
      placeholder="輸入姓名、申請項目或狀態"
    />
  </Field>

  {dataState === "loading" && (
    <div role="status">
      <span className="sr-only">
        申請紀錄載入中
      </span>

      <LoadingTable />
    </div>
  )}

  {dataState === "error" && (
    <Alert
      variant="destructive"
      role="alert"
    >
      <AlertTitle>
        無法載入申請紀錄
      </AlertTitle>

      <AlertDescription>
        取得資料時發生錯誤,請稍後再試。
      </AlertDescription>

      <AlertAction>
        <Button
          variant="outline"
          size="sm"
          onClick={handleRetry}
        >
          重新載入
        </Button>
      </AlertAction>
    </Alert>
  )}

  {dataState === "success" &&
    applications.length === 0 && (
      <EmptyState>
        <EmptyStateTitle>
          尚無申請紀錄
        </EmptyStateTitle>

        <EmptyStateDescription>
          目前沒有任何申請資料。
        </EmptyStateDescription>
      </EmptyState>
    )}

  {dataState === "success" &&
    applications.length > 0 &&
    filteredApplications.length === 0 && (
      <EmptyState>
        <EmptyStateTitle>
          找不到符合條件的資料
        </EmptyStateTitle>

        <EmptyStateDescription>
          請嘗試其他搜尋關鍵字。
        </EmptyStateDescription>

        <EmptyStateAction>
          <Button
            variant="outline"
            onClick={() => setQuery("")}
          >
            清除搜尋
          </Button>
        </EmptyStateAction>
      </EmptyState>
    )}

  {dataState === "success" &&
    filteredApplications.length > 0 && (
      <>
        <p
          aria-live="polite"
          className="text-sm text-muted-foreground"
        >
          共 {filteredApplications.length} 筆資料
        </p>

        <ApplicationTable
          applications={filteredApplications}
        />

        <Pagination aria-label="申請紀錄分頁">
          ...
        </Pagination>
      </>
    )}
</section>

現在整個 Data List 已經不再只有:

有資料

這一種世界。


25. State Machine 開始出現了

如果把今天的狀態畫成流程:

                  ┌─────────┐
                  │ Loading │
                  └────┬────┘
                       │
             ┌─────────┴─────────┐
             ↓                   ↓
         Success               Error
             │                   │
      ┌──────┼──────┐            │
      ↓      ↓      ↓            │
    Data   Empty  No Results      │
                              Retry
                                │
                                └──→ Loading

這其實已經有一點:

State Machine

的味道了。

我們目前不用真的引入 State Machine Library。

但先把狀態關係想清楚,就能避免很多:

isLoading && ...
!isLoading && !error && ...
!isLoading && error && ...
data.length === 0 && ...
query && ...

最後 JSX 變成條件判斷地獄。


26. Data State 和 Component State 不一樣

前面做 Input 時,我們處理:

Default
Focus
Disabled
Invalid

這些比較像:

Component State

今天處理:

Loading
Success
Empty
No Results
Error

則比較像:

Application / Data State

CUI 不需要把 Application State 全部塞進 Component。

例如我們不會做:

<Table
  loading
  error
  empty
  noResults
/>

然後讓 Table 自己決定所有東西。

而是:

Skeleton
Alert
Empty State
Table
Pagination

由 Data List Pattern 根據狀態組合。


27. 為什麼不做超級 <DataTable />?

現在其實已經很容易產生這個念頭:

<DataTable
  data={data}
  loading={loading}
  error={error}
  searchable
  pagination
  emptyMessage="..."
  onRetry={...}
/>

看起來超方便。

但很快就會長成:

<DataTable
  searchable
  sortable
  filterable
  selectable
  paginated
  loading
  empty
  error
  striped
  sticky
  compact
  responsive
  ...
/>

😇

所以目前 CUI 的方向還是:

Small Components
+
Clear Contracts
+
Composition Patterns

而不是:

One Component To Rule Them All

28. Legacy 怎麼處理這些 State?

這也是今天很有趣的地方。

React:

{isLoading && <Skeleton />}

Legacy 沒有 React State。

但它一樣可以:

<section
  data-cui-pattern="data-list"
  aria-busy="true"
>
  <div data-cui-slot="skeleton"></div>
</section>

成功後 Server Render:

<section
  data-cui-pattern="data-list"
  aria-busy="false"
>
  <table data-cui-slot="table">
    ...
  </table>
</section>

Error:

<div
  data-cui-slot="alert"
  data-variant="destructive"
  role="alert"
>
  ...
</div>

也就是:

React
→ 用 State 決定 render 什麼

Legacy
→ Server / JavaScript 決定 render 什麼

CUI
→ 提供相同的 UI Contract

這正是我們一開始設計:

data-cui-*

的目的。


29. Reduced Motion 也值得注意

Skeleton 常見:

animate-pulse

但不是每個使用者都喜歡持續動畫。

Tailwind / CSS 可以配合:

prefers-reduced-motion

例如之後可以讓:

Reduced Motion
→ 不 pulse

或降低動畫強度。

這不是今天一定要完整實作的功能,但要記住:

Loading Animation 也是 Motion。

Accessibility 不只有:

Color
Keyboard
Screen Reader

Motion preference 也是其中一部分。


30. Accessibility Check

今天最後完整檢查:

Loading

✓ aria-busy
✓ 有「載入中」的文字資訊
✓ Skeleton 不承擔 Accessible Name
✓ Layout 不大幅跳動

Success

✓ Result Count
✓ Semantic Table
✓ Badge 不只靠顏色
✓ Pagination 有清楚的 Navigation Label

Empty

✓ 明確說明目前沒有資料
✓ 不誤稱為 Error
✓ 必要時提供新增資料 Action

No Results

✓ 說明搜尋沒有結果
✓ 與真正 Empty 區分
✓ 提供清除 / 修改搜尋的下一步

Error

✓ 明確告知載入失敗
✓ 不把 Error 說成「沒有資料」
✓ 必要時使用 role="alert"
✓ 提供 Retry
✓ 不直接暴露技術錯誤

31. Keyboard Test

今天 Keyboard Test 也比以前更重要。

測:

Success
Loading
Empty
No Results
Error
Retry

尤其注意:

State 改變後
Focus 有沒有突然亂跳?

正常情況:

Search
↓
Loading
↓
Result

Focus 不應該自己跑掉。

Error:

Retry Button
↓ Enter
Loading
↓ Success

也要觀察 Retry Button 消失後 Focus 的實際行為。

先記錄問題,不要看到任何 Dynamic UI 就急著用 JavaScript 強制 focus。


32. Screen Reader 思考方式

今天可以試著不看畫面,只想像使用者會聽到什麼。

Loading:

申請紀錄
申請紀錄載入中

Success:

共 3 筆資料
申請紀錄表格
姓名
申請項目
狀態
更新時間
...

Empty:

尚無申請紀錄
目前沒有任何申請資料

No Results:

找不到符合條件的資料
請嘗試其他搜尋關鍵字
清除搜尋,按鈕

Error:

無法載入申請紀錄
取得資料時發生錯誤,請稍後再試
重新載入,按鈕

如果只聽這些資訊,也能知道:

現在發生什麼事
+
下一步可以做什麼

那這個 State Pattern 就已經相當清楚了。


33. 最後執行檢查

完成後:

npm run build
npm run lint

以及我們的 Contrast Check:

npm run check:contrast

確認:

git status
git diff

如果今天新增:

src/components/ui/skeleton.tsx

並修改:

src/App.tsx

可以:

git add src/components/ui/skeleton.tsx src/App.tsx

Commit:

git commit -m "feat: add data list states"

Day 21 完成

今天表面上做的是:

Loading
Empty
Error

但真正開始處理的是:

Interface State。

以前我們比較關心:

Button 能不能按?
Input 有沒有 Label?
Table Header 有沒有 scope?
Pagination 有沒有 aria-current?

現在問題變成:

資料還沒來時,使用者知道嗎?

資料真的為空時,使用者知道嗎?

搜尋不到時,使用者知道原因嗎?

API 壞掉時,使用者會不會誤以為沒有資料?

發生錯誤後,使用者知道下一步嗎?

這已經從:

Accessible Component

慢慢走向:

Accessible Experience

而今天最值得記住的是:

Loading ≠ Empty
Empty ≠ No Results
No Results ≠ Error

畫面上它們可能都只是:

「沒有 Table Rows」

但語意完全不同。


到目前為止,我們的 Data List 已經長這樣

Data List Pattern
│
├── Search
│   └── Field + Input
│
├── Feedback
│   └── Result Count
│
├── Loading
│   └── Skeleton
│
├── Success
│   ├── Table
│   │   └── Badge
│   └── Pagination
│
├── Empty
│   └── Empty State
│
├── No Results
│   └── Empty State + Clear Search
│
└── Error
    └── Alert + Retry

到這裡,我們已經不是單純在蒐集 UI Components 了。

我們開始建立:

一套元件應該怎麼合作的規則。


Next:Day 22

目前 CUI 已經有不少元件:

Button
Input
Field
Textarea
Checkbox
Radio
Switch
Select
Badge
Alert
Empty State
Table
Pagination
Skeleton

元件一多,下一個問題就開始出現:

為什麼這個叫 size="default"
另一個叫 size="md"?

為什麼這個用 data-invalid
另一個用 aria-invalid?

data-cui-slot 有沒有全部一致?

variant 的名稱是不是開始亂掉?

disabled / loading / invalid 到底誰負責?

如果現在不整理,再做十顆元件之後一定會開始:

「欸,我之前到底怎麼命名的?」🤣

所以 Day 22 我們先暫停新增元件,來做一次 CUI 的中期整理。

Day 22:

元件變多了:整理 CUI Component Contract

我們會正式盤點:

Naming
Variant
Size
State
ARIA
data-cui-*
React API
Legacy Contract

讓 CUI 從:

一堆做好的元件

開始真正變成:

有一致規則的 Design System。


上一篇
Day 20:組出第一個 Data List:元件開始真的拿來做系統
下一篇
Day 22: 元件越做越多:整理 CUI Component Contract
系列文
30 天打造 Accessible UI Kit:從 shadcn/ui 到自己的 Design System 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言